package na.learn.java.corepatterns.caching;

import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Map;

import na.learn.java.Logging.CacheLogger;

import org.apache.log4j.Logger;

/*   1. Algorithm for cleanup LRU entries:
 *   1.a.) cleanup : call following sub-phases: 
 *   
 *   while(!isHealthy()) call subPhase(counter++);
 *   subPhase(phase #): switch(phase#)
 *   {
 *    case 1: Young Queue stale cleanup:
 *    		  		Lock YoungQueue; (Notify and/or Remove) stale keys from YoungQueue,
 *    					and corresponding entries from HashMap.
 *   			break;
 *    case 2: Old Queue stale cleanup:
 *    		  		Lock OldQueue; (Notify and/or * Remove) stale keys from OldQueue,
 *    					and corresponding entries from HashMap.
 *     			  	If(OldQueue has vacancy) then Lock both Queues;
 *     	 				Move some keys from YoungQueue to OldQueue, as per vacancy
 * 			 	break; 
 * 	  case 3: Remove Oldest keys, and move some from YoungQueue to OldQueue:
 * 			  		Lock OldQueue; Find oldest keys from OldQueue and (Notify and/or Remove) 
 * 						them along with corresponding entries from HashMap.
 * 			  		If(OldQueue has vacancy) then Lock both Queues;
 * 					Move some keys from YoungQueue to OldQueue, as per vacancy.
 * 				
 * 
 */
public class PhasedLRUCleaner implements Cleaner {

	// 	Reference to young Queue used by Cache.
	private FIFOQueue youngQueue;

	//	 Reference to old Queue used by Cache.
	private FIFOQueue oldQueue;

	private Map cKeyValue;

	// init is used to find if the cleaner has been initialized with the data structures used by Cache.
	// These Data Structures passed by Cache class implementation, are locked by Cleaner and cleaned up in 3 phases.
	boolean init = false;

	// Logger Instance
	private static Logger logger;

	static {
		logger = CacheLogger.getLogger(PhasedLRUCleaner.class.getName());
	}

	public PhasedLRUCleaner() {

	}

	// Attach data structures ( keyValueHashMap, YoungQueue, and OldQueue passed by Cache implementation. 
	public void attach(Map cKeyValue, FIFOQueue youngQueue, FIFOQueue oldQueue) {
		if (!init) {
			logger.info("Data Structures atached to Cleaner.");
			this.cKeyValue = cKeyValue;
			this.youngQueue = youngQueue;
			this.oldQueue = oldQueue;
			init = true;
		}

	}

	/**
	 * Take lock on both queues.
	 * Until size of youngQueue is below threashOld, call different phases one after another. 
	 */
	public void run() {

		int phaseCounter = 1;

		while (true) {
			// try {

			synchronized (youngQueue) {
				synchronized (oldQueue) {
					logger.debug("Sarting Cleanup.");
					if (youngQueue.size() >= youngQueue.getThreashold())
						phaseCounter = cleanup(phaseCounter);
					else
						phaseCounter = 0;

					oldQueue.notifyAll();
				}
				youngQueue.notifyAll();
				logger.debug("Cleanup complete.");
			}

			while (youngQueue.size() <= youngQueue.getThreashold())
				;

		}

	}

	/**
	 * Phase 1:		Remove Stale entries from Young Queue/HashMap.
	 * Phase 2A:	Remove Stale entries from Old Queue/HashMap.
	 * Phase 2B:	Move keys from YoungQueue to OldQueue/HashMap, as per vacancy.
	 * Phase 3A:	Remove Oldest entries from Oldqueue/HashMap.
	 * Phase 3A:	Move keys from YoungQueue to OldQueue/HashMap, as per vacancy. 
	 * @param phaseCounter
	 * @return
	 */
	protected int cleanup(int phaseCounter) {
		switch (phaseCounter) {
		case 1:
			System.out.println("Phase 1");
			removeStaleEntries(youngQueue);
			return 2;
		case 2:
			System.out.println("Phase 2");
			removeStaleEntries(oldQueue);
			moveFromYoungToOld();
			return 3;
		case 3:
			System.out.println("Phase 3");
			removeOldest();
			moveFromYoungToOld();
			return 1;
		default:
			return 1;
		}

	}

	/**
	 *  Remove Stale entries from Young/Old  Queue and corresponding entries from HashMap.
	 * @param queue
	 */
	protected void removeStaleEntries(FIFOQueue queue) {
		long currentTime = System.currentTimeMillis();

		logger.info("Starting to remove Stale Entries");
		logger.debug("Start of removeStaleEntries from :" + queue.getName() + " having size: " + queue.size());

		for (ListIterator iter = queue.listIterator(); iter.hasNext();) {
			CompositeKey key = (CompositeKey) (iter.next());
			if (key.getEndTime() < currentTime) {

				logger.debug("Queue Size =" + queue.size() + ",  key: " + key.getKey() + ", EndTime:  " + key.getEndTime() + ", SystemTime: " + System.currentTimeMillis());

				cKeyValue.remove(key);
				iter.remove();
				key = null;
				}
		}
		logger.debug("End of removeStaleEntries : queueSize" + queue.size());
		//queue.notifyAll();

	}

	/**
	 * 
	 * Move keys from YoungQueue to OldQueue/HashMap, as per vacancy. 
	 */
	protected void moveFromYoungToOld() {
		if (oldQueue.size() >= oldQueue.getMaxAllocSize())
			return;
		logger.info("Starting to move Young Entries to Old queue.");
		
		// Calculate vacancies in old Queue.
		int oldQSize = oldQueue.size();
		int oldQTOld = oldQueue.getThreashold();
		int numOfVacancies = oldQueue.getMaxAllocSize() - (oldQSize < oldQTOld ? oldQSize : oldQTOld);
		
		// Move from young to Old Q.
		for (int counter = 1; counter <= numOfVacancies && youngQueue.size() > 0; counter++) {

			CompositeKey cKey = (CompositeKey) (((FIFOQueue) youngQueue).element());
			oldQueue.add(cKey);
			((FIFOQueue) youngQueue).remove();
			
			logger.debug("Phase 2: moveFromYoungToOld moving key " + cKey.getKey().toString() + " from young to old Queue.");
		}
	}

	/**
	 * Remove Oldest entries from Oldqueue/HashMap.
	 *
	 */
	protected void removeOldest() {
		int vacancies = oldQueue.getMaxAllocSize() - oldQueue.getThreashold();
		for (int count = 1; count <= vacancies; count++) {
			Object key = ((CompositeKey) ((FIFOQueue) oldQueue).element()).getKey();
			Object value = oldQueue.element();
			logger.debug("Phase 3: removeOldest() : removing key " + key.toString());
			
			// If object implements Cacheable, notify it.
			if (key instanceof Cacheable)
				((Cacheable) key).notifyRemoval();
			if (value instanceof Cacheable)
				((Cacheable) value).notifyRemoval();
			
			// Remove entries from Queue, Map, and set reference to Null to allow them GCed.
			value = null;
			cKeyValue.remove(key);
			((LinkedList) oldQueue).remove();
		}
		System.gc();

	}

	/**
	 * Are Data Structures attached by Cache to this cleaner object. 
	 */
	public boolean isInit() {
		return init;
	}

}
